package com.beta.cat.utils.http;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLInitializationException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author gwqin
 * @description HTTP请求工具类，基于Spring的RestTemplate和Apache的HttpClient封装。
 * <pre>
 *     1、基本使用方式：
 *      HttpManager.getInstance().get("http://www.xxxx.com/xxxx", String.class);
 *
 *     2、自定义连接池：
 *      HttpManager.getInstance(new HttpConfig().setMaxConnectionSizeTotal(100).setMaxConnectionSizePerRoute(20)).get("http://www.xxxx.com/xxxx", String.class);
 *      或者
 *      HttpManager httpManager = HttpManager.getInstance(new HttpConfig().setMaxConnectionSizeTotal(100).setMaxConnectionSizePerRoute(20));
 *      httpManager.get("http://www.xxxx.com/xxxx", String.class);
 *
 *     3、自定义代理：
 *      HttpManager.getInstance(new HttpConfig().setHttpProxy("xxx.xx.xx.xxx", 80)).get("http://www.xxxx.com/xxxx", String.class);
 *
 *     4、自定义连接池&代理：
 *      HttpManager.getInstance(new HttpConfig().setMaxConnectionSizeTotal(100).setMaxConnectionSizePerRoute(20).setHttpProxy("xxx.xx.xx.xxx", 80, "userName", "password")).get("http://www.xxxx.com/xxxx", String.class);
 *
 *     本类中提供的Restful风格API和RestTemplate官方提供的API使用方式一致，可参考RestTemplate官方文档。
 * <pre/>
 * @date 2022-09-19 19:33
 */
public class HttpManager {

    /********************************** 【类属性】- 开始 **********************************/

    private static final Object LOCK = new Object();

    /**
     * RestTemplate实例，引用restTemplates缓存中的实例
     */
    private RestTemplate instance = null;

    /**
     * 不同配置的RestTemplate实例
     */
    private static final Map<String, RestTemplate> REST_TEMPLATES = new ConcurrentHashMap<>();

    /**
     * 默认的RestTemplate配置
     */
    private static final HttpConfig DEFAULT_REST_CONFIG = new HttpConfig();

    /**
     * 保存各执行线程私有的RestTemplateUtil实例，用于请求级别设置超时时间
     *
     * @see HttpManager#getRequestFactory
     */
    private static final ThreadLocal<HttpManager> LOCAL_THIS = new ThreadLocal<>();

    /**
     * 从连接池获取连接的超时时间，默认30秒
     */
    private static final Integer DEFAULT_CONNECTION_REQUEST_TIMEOUT = 30 * 1000;

    /**
     * 建立连接超时时间，默认30秒
     */
    private static final Integer DEFAULT_CONNECT_TIMEOUT = 30 * 1000;

    /**
     * 请求超时时间，默认60秒
     */
    private static final Integer DEFAULT_SOCKET_TIMEOUT = 60 * 1000;

    /**
     * 从连接池获取连接的超时时间，默认30秒
     */
    private Integer connectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;

    /**
     * 建立连接超时时间，默认30秒
     */
    private Integer connectTimeout = DEFAULT_CONNECT_TIMEOUT;

    /**
     * 请求超时时间，默认60秒
     */
    private Integer socketTimeout = DEFAULT_SOCKET_TIMEOUT;

    /********************************** 【类属性】- 结束 **********************************/

    /********************************** 【实例化方法】- 开始 **********************************/

    /**
     * 获取默认的实例
     *
     * @return {@link HttpConfig}
     */
    public static HttpManager getInstance() {
        return getInstance(DEFAULT_REST_CONFIG);
    }

    /**
     * 获取自定义配置{@link HttpConfig}的实例
     *
     * @param httpConfig
     * @return {@link HttpManager}
     */
    public static HttpManager getInstance(HttpConfig httpConfig) {
        return new HttpManager(httpConfig);
    }

    /**
     * 工具类，私有化构造方法
     *
     * @param httpConfig 连接池及代理配置
     */
    private HttpManager(HttpConfig httpConfig) {
        initRestTemplate(httpConfig);
    }

    /**
     * 初始化RestTemplate实例，先从缓存获取已经存在的实例，缓存没有的话则自动创建并加入缓存
     *
     * @param httpConfig 配置
     * @return RestTemplate实例
     */
    private void initRestTemplate(HttpConfig httpConfig) {
        if (httpConfig == null) {
            // 如果传入的配置为null则给个默认配置
            httpConfig = DEFAULT_REST_CONFIG;
        }

        String restConfigKey = httpConfig.getKey();
        // 判断内存中是否已存在对应配置的RestTemplate实例，当不存在对应配置的实例时执行内部逻辑，构建一个新的RestTemplate实例
        if (!REST_TEMPLATES.containsKey(restConfigKey)) {
            // 加锁
            synchronized (LOCK) {
                // 二次判断内存中是否存在对应实例
                if (!REST_TEMPLATES.containsKey(restConfigKey)) {
                    // 如果没有对应配置的RestTemplate实例，则使用配置实例化一个新的实例，并加入内存
                    HttpComponentsClientHttpRequestFactory httpRequestFactory = getRequestFactory(httpConfig);
                    REST_TEMPLATES.put(restConfigKey, new RestTemplate(httpRequestFactory));
                }
            }
        }

        instance = REST_TEMPLATES.get(restConfigKey);
    }

    /********************************** 【实例化方法】- 结束 **********************************/

    /********************************** 【属性设置】- 开始 **********************************/

    /**
     * 从连接池获取连接的超时时间
     *
     * @param connectionRequestTimeout 获取连接的超时时间，单位：毫秒
     * @return {@link HttpManager}
     */
    public HttpManager setConnectionRequestTimeout(Integer connectionRequestTimeout) {
        if (connectionRequestTimeout == null || connectionRequestTimeout < 1) {
            connectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
        }
        this.connectionRequestTimeout = connectionRequestTimeout;
        return this;
    }

    /**
     * 建立连接超时时间
     *
     * @param connectTimeout 建立连接超时时间，单位：毫秒
     * @return {@link HttpManager}
     */
    public HttpManager setConnectTimeout(Integer connectTimeout) {
        if (connectTimeout == null || connectTimeout < 1) {
            connectTimeout = DEFAULT_CONNECT_TIMEOUT;
        }
        this.connectTimeout = connectTimeout;
        return this;
    }

    /**
     * 请求超时时间
     *
     * @param socketTimeout 超时时间，单位：毫秒
     * @return {@link HttpManager}
     */
    public HttpManager setSocketTimeout(Integer socketTimeout) {
        if (socketTimeout == null || socketTimeout < 1) {
            socketTimeout = DEFAULT_SOCKET_TIMEOUT;
        }
        this.socketTimeout = socketTimeout;
        return this;
    }

    /**
     * 得到获取连接的超时时间
     *
     * @return
     */
    public Integer getConnectionRequestTimeout() {
        return connectionRequestTimeout;
    }

    /**
     * 获取建立连接超时时间
     *
     * @return
     */
    public Integer getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * 获取响应的超时时间
     *
     * @return
     */
    public Integer getSocketTimeout() {
        return socketTimeout;
    }

    /********************************** 【属性设置】- 结束 **********************************/

    /********************************** 【核心业务方法】- 开始 **********************************/

    /**
     * get请求
     *
     * @param url          接口地址
     * @param responseType 返回类型
     * @param <T>
     * @return
     * @throws RestClientException
     */
    public <T> T get(String url, Class<T> responseType) throws RestClientException {
        return exchange(url, HttpMethod.GET, null, responseType, new HashMap<>(0)).getBody();
    }

    /**
     * get请求
     *
     * @param url          接口地址
     * @param responseType 返回类型
     * @param headers      请求头
     * @param <T>
     * @return
     * @throws RestClientException
     */
    public <T> T get(String url, Class<T> responseType, Map<String, String> headers) throws RestClientException {
        MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
        headers.forEach(multiValueMap::add);
        HttpEntity<String> requestEntity = new HttpEntity<>(multiValueMap);
        return exchange(url, HttpMethod.GET, requestEntity, responseType, new HashMap<>(0)).getBody();
    }

    /**
     * post请求
     *
     * @param url          接口地址
     * @param responseType 返回类型
     * @param <T>
     * @return
     * @throws RestClientException
     */
    public <T> T post(String url, Class<T> responseType) throws RestClientException {
        return exchange(url, HttpMethod.POST, null, responseType, new HashMap<>(0)).getBody();
    }

    /**
     * post请求
     *
     * @param url          接口地址
     * @param responseType 返回类型
     * @param body         请求体
     * @param <T>
     * @return
     * @throws RestClientException
     */
    public <T> T post(String url, Class<T> responseType, String body) throws RestClientException {
        HttpEntity<String> requestEntity = new HttpEntity<>(body);
        return exchange(url, HttpMethod.POST, requestEntity, responseType, new HashMap<>(0)).getBody();
    }

    /**
     * post请求
     *
     * @param url          接口地址
     * @param responseType 返回类型
     * @param body         请求体
     * @param headers      请求头
     * @param <T>
     * @return
     * @throws RestClientException
     */
    public <T> T post(String url, Class<T> responseType, String body, Map<String, String> headers) throws RestClientException {
        MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
        headers.forEach(multiValueMap::add);
        HttpEntity<String> requestEntity = new HttpEntity<>(body, multiValueMap);
        return exchange(url, HttpMethod.POST, requestEntity, responseType, new HashMap<>(0)).getBody();
    }

    /**
     * 对给定的URI模板执行HTTP方法，将给定的请求实体写入请求，并将响应作为ResponseEntity返回。
     * 当其他get和post方法不支持业务需求时可使用该方法，例如：上传文件。
     *
     * @param url           接口地址
     * @param method        请求方法类型
     * @param requestEntity HTTP请求实体，由请求头和请求体组成
     * @param responseType  返回类型
     * @param uriVariables  URL参数列表，Map结构，key为参数名称，value为参数值。例如：<br/>
     *                      URL：http://xxxx/abc?name={name}&age={age} <br/>
     *                      则构造uriVariables参数参考如下：<br/>
     *                      <pre> {@code
     *                          Map<String, Object> uriVariables = new HashMap<>();
     *                          uriVariables.put("name","zhangsan");
     *                          uriVariables.put("age", 10);<br/>
     *                          }
     *                      </pre>
     *                      <b>上传文件示例：<b/>
     *                      <pre> {@code
     *                          public String uploadFile() {
     *                                File file = new File("C:\\Users\\beta\\Desktop\\betalicense.lc");
     *                                // 1、封装请求头
     *                                HttpHeaders headers = new HttpHeaders();
     *                                headers.setContentType(MediaType.parseMediaType("multipart/form-data"));
     *                                // 2、封装请求体
     *                                MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
     *                                FileSystemResource resource = new FileSystemResource(file);
     *                                param.add("file", resource);
     *                                param.add("fileSavePath", "16/file/20221008/betalicense.lc");
     *                                param.add("type", "uploadfile");
     *                                // 3、封装整个请求报文
     *                                HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(param, headers);
     *                                // 4、发送请求
     *                                String url = "https://w-1.test.betawm.com/Beta.FileServiceV2/Api/OrgFile";
     *                                ResponseEntity<String> data = RestTemplateUtil.getInstance().exchange(url, HttpMethod.POST, formEntity, String.class, new HashMap<>(0));
     *                                // 5、请求结果处理
     *                                System.out.println(data.getBody());
     *                          }
     *                       }
     *                      </pre>
     * @param <T>
     * @return ResponseEntity
     * @throws RestClientException
     */

    public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
        try {
            LOCAL_THIS.set(this);
            return instance.exchange(url, method, requestEntity, responseType, uriVariables);
        } finally {
            LOCAL_THIS.remove();
        }
    }

    /********************************** 【核心业务方法】- 结束 **********************************/

    /********************************** 【内部辅助性方法】- 开始 **********************************/

    /**
     * 创建HttpComponentsClientHttpRequestFactory
     * 支持代理设置、连接池参数配置
     *
     * @param httpConfig
     * @return
     */
    private HttpComponentsClientHttpRequestFactory getRequestFactory(final HttpConfig httpConfig) {
        HttpClientBuilder httpClientBuilder = acceptsUntrustedCertsHttpClient(httpConfig);
        // 设置代理
        HttpConfig.HttpProxy httpProxy = httpConfig.getHttpProxy();
        if (httpProxy != null) {
            // 配置代理
            String httpProxyHost = httpProxy.getHttpProxyHost();
            Integer httpProxyPort = httpProxy.getHttpProxyPort();
            HttpHost proxy = new HttpHost(httpProxyHost, httpProxyPort);

            //设置给定身份验证作用域的凭据
            String userName = httpProxy.getUserName();
            String password = httpProxy.getPassword();
            if (userName != null && password != null) {
                CredentialsProvider provider = new BasicCredentialsProvider();
                provider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials(userName, password));
                httpClientBuilder.setDefaultCredentialsProvider(provider);
            }

            httpClientBuilder.setProxy(proxy);
        }

        // 设定单次请求时设置的超时时间，会覆盖默认的超时配置
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory() {
            @Override
            protected void postProcessHttpRequest(HttpUriRequest request) {
                if (request instanceof HttpRequestBase) {
                    ((HttpRequestBase) request).setConfig(RequestConfig.custom()
                            /**
                             * 从当前请求中获取用户设定的超时时间，通过调用setXXXTimeout(XXX)设置的超时时间
                             */
                            // 获取连接的超时时间
                            .setConnectionRequestTimeout(LOCAL_THIS.get().getConnectionRequestTimeout())
                            // 建立连接的超时时间
                            .setConnectTimeout(LOCAL_THIS.get().getConnectTimeout())
                            // 获取响应的超时时间
                            .setSocketTimeout(LOCAL_THIS.get().getSocketTimeout())
                            .build());
                }
            }
        };

        // 设置默认的超时时间
        httpRequestFactory.setHttpClient(httpClientBuilder.build());
        httpRequestFactory.setHttpContextFactory((httpMethod, uri) -> {
            RequestConfig customRequestConfig = RequestConfig.custom()
                    /**
                     * 设置默认的超时时间
                     */
                    // 获取连接的超时时间
                    .setConnectionRequestTimeout(DEFAULT_CONNECTION_REQUEST_TIMEOUT)
                    // 建立连接的超时时间
                    .setConnectTimeout(DEFAULT_CONNECT_TIMEOUT)
                    // 获取响应的超时时间
                    .setSocketTimeout(DEFAULT_SOCKET_TIMEOUT).build();
            HttpContext context = HttpClientContext.create();
            context.setAttribute(HttpClientContext.REQUEST_CONFIG, customRequestConfig);
            return context;
        });

        return httpRequestFactory;
    }

    /**
     * 构建HttpClientBuilder，支持http和https
     *
     * @param httpConfig
     * @return
     */
    private static HttpClientBuilder acceptsUntrustedCertsHttpClient(HttpConfig httpConfig){
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // setup a Trust Strategy that allows all certificates.
        SSLContext sslContext = null;
        try {
            sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
        } catch (Exception e) {
            throw new SSLInitializationException(e.getMessage(), e);
        }

        HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);

        // 设置可关闭的httpclient
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslSocketFactory)
                .build();
        // 创建ConnectionManager，添加Connection配置信息
        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        // 连接池最大连接数
        poolingConnectionManager.setMaxTotal(httpConfig.getMaxConnectionSizeTotal());
        // 每个主机的并发
        poolingConnectionManager.setDefaultMaxPerRoute(httpConfig.getMaxConnectionSizePerRoute());

        // 用来释放不用的连接
        new HttpClientConnectionMonitorThread(poolingConnectionManager).start();

        httpClientBuilder.setConnectionManager(poolingConnectionManager);

        return httpClientBuilder;
    }

    /********************************** 【内部辅助性方法】- 结束 **********************************/

    /********************************** 【内部资源回收】- 开始 **********************************/

    /**
     * 连接池监控线程。作用：定时关闭过期的或空闲的连接，节省服务资源
     */
    private static class HttpClientConnectionMonitorThread extends Thread {

        private final PoolingHttpClientConnectionManager connManager;
        private volatile boolean shutdown = false;

        public HttpClientConnectionMonitorThread(PoolingHttpClientConnectionManager connManager) {
            super();
            this.setName("http-connection-monitor");
            this.setDaemon(true);
            this.connManager = connManager;
        }

        @Override
        public void run() {
            while (!shutdown) {
                try {
                    synchronized (this) {
                        // 等待5秒（官方示例）
                        wait(5000);

                        // 关闭过期的链接
                        connManager.closeExpiredConnections();

                        // 选择关闭 空闲30秒的链接
                        connManager.closeIdleConnections(30, TimeUnit.SECONDS);
                    }
                } catch (InterruptedException ex) {
                    // 发生异常时进行捕获，不会导致回收线程中断，会继续下一轮回收工作
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /********************************** 【内部资源回收】- 结束 **********************************/
}
